package com.chaos.meta.config;

import com.alibaba.fastjson.JSONObject;
import com.chaos.meta.entity.User;
import com.chaos.meta.common.SessionManager;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationContext;
import org.springframework.context.event.GenericApplicationListenerAdapter;
import org.springframework.context.event.SmartApplicationListener;
import org.springframework.core.env.Environment;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.context.DelegatingApplicationListener;
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
import org.springframework.security.web.csrf.CsrfFilter;
import org.springframework.security.web.csrf.CsrfToken;
import org.springframework.security.web.session.ConcurrentSessionFilter;
import org.springframework.security.web.util.matcher.RequestMatcher;

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.net.URLEncoder;
import java.util.*;

/**
 * @author naicha
 */
//@Component
//@EnableWebSecurity
//@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter
{
    /**
     * 免登录的接口
     */
    @Override
    public void configure(WebSecurity web){
        List<String> loginIgnoreUrl = Arrays.asList("/static/**");
//        loginIgnoreUrl.add("/**/**"); //全部免登录，调测使用
        web.ignoring().antMatchers(loginIgnoreUrl.toArray(new String[0]));
    }

    /**
     * 自定义登录认证
     */
//    @Autowired
    private LoginValidateAuthenticationProvider loginValidateAuthenticationProvider;

    /**
     * 自定义登录信息
     */
//    @Autowired
    private CustomAuthenticationDetailsSource customAuthenticationDetailsSource;

//    @Autowired
    private Environment environment;

    @Value("${server.servlet.session.timeout}")
    private Integer sessionTimeOut;

    private static final String SERVER_PATH_KEY = "server.servlet.context-path";

    /**
     * 权限核心配置
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 基础设置, 配置HTTP基本身份验证
        http.httpBasic().and().authorizeRequests().anyRequest().authenticated()
                .and().formLogin().loginProcessingUrl("/login")
                // 成功登录处理器
                .successHandler((req, resp, authentication)->{
                    // 将用户及世界基本信息保存到cookie
                    User user = (User) authentication.getPrincipal();
                    CsrfToken csrf = (CsrfToken) req.getAttribute(CsrfToken.class.getName());
                    Map<String, Object> userInfoMap = new HashMap<String, Object>(){
                        {
                            put("userName", URLEncoder.encode(user.getUserName(), "utf-8"));
                            // 记住登录
                            put("JSESSIONID", req.getSession().getId());
                            put("XSRF-TOKEN", csrf.getToken());
                        }
                    };

                    userInfoMap.forEach((k, v)->{
                        Cookie cookie = new Cookie(k, v.toString());
                        cookie.setMaxAge(sessionTimeOut);
                        resp.addCookie(cookie);
                    });
                    resultHandle(resp, "0", "login success!");
                })
                // 失败登录处理器
                .failureHandler((req, resp, exception)->
                    resultHandle(resp, "5", exception.getMessage())
                )
                // 登录成功后有权限访问所有页面
                .permitAll()
                .authenticationDetailsSource(customAuthenticationDetailsSource);

        // session authenticate异常处理, csrf异常处理
        http.exceptionHandling()
                .authenticationEntryPoint((req, resp, authentication)->{
                    resp.sendRedirect("/Login.html");
                    resultHandle(resp, "6", "please login!");
                })
                .accessDeniedHandler((request, response, accessDeniedException)->{
                    resultHandle(response, "3", "No access!");
                });

        // 防止跨站请求伪造攻击
        http.csrf().ignoringAntMatchers("/login").requireCsrfProtectionMatcher(new CustomMatcher())
                .and().csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());

        CustomSessionRegistry sessionRegistry = getSessionRegistry(http);
        SessionManager.init(sessionRegistry);
        http.sessionManagement().sessionAuthenticationStrategy(
                new CustomControlAuthenticationStrategy(sessionRegistry, 1));

        http.logout()
                .logoutUrl("/logout")
                .addLogoutHandler((req, resp, authentication)->{
                    SessionManager.sessionRegistry.removeSessionInformation(req.getSession().getId());
                    Cookie cookie = new Cookie("userName", null);
                    cookie.setPath(environment.getProperty(SERVER_PATH_KEY));
                    cookie.setMaxAge(0);
                    resp.addCookie(cookie);
                })
                .logoutSuccessHandler((req,resp,authentication)->{
                    resultHandle(resp, "0", "logout success!");
                })
                .permitAll();

        http.headers().frameOptions().disable();

        // 安全防护：开启xss过滤
        http.addFilterAfter(new XssFilter(), CsrfFilter.class);

        // 对过期session进行过滤，配合CustomControlAuthenticationStrategy使用
        http.addFilterAfter(new ConcurrentSessionFilter(sessionRegistry, (sessionInformationExpiredEvent )->{
            HttpServletResponse resp = sessionInformationExpiredEvent.getResponse();
            resp.sendRedirect("/Login.html");
            resultHandle(resp, "4", "login is expired!");
        }), XssFilter.class);
    }

    private void resultHandle(HttpServletResponse resp, String resultCode, String resultMessage) throws IOException {
        JSONObject resultJson = new JSONObject();
        resultJson.put("resultData", null);
        resultJson.put("resultCount", 0);
        resultJson.put("resultCode", resultCode);
        resultJson.put("resultMessage", resultMessage);
        resp.setContentType("application/json;charset=utf-8");
        PrintWriter out = resp.getWriter();
        out.write(resultJson.toJSONString());
        out.flush();
        out.close();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth){
        auth.authenticationProvider(loginValidateAuthenticationProvider);
    }

    /**
     *  自定义csrf过滤接口配置
     *  需要鉴权的get请求，免登录get请求无效：WebSecurityCsrfFilter拦截器顺序在CsrfFilter之前
     *  ignoringMethods中填写ServletPath,如需使用通配符请用正则表达式，如"^(/websocket/).+"表示/websocket/**
     * */
    public class CustomMatcher implements RequestMatcher
    {
        private final HashSet<String> ignoringMethods = new HashSet<>(
                Arrays.asList("/example/queryById"));

        @Override
        public boolean matches(HttpServletRequest request)
        {
            boolean allowedMethod = !"GET".equalsIgnoreCase(request.getMethod());
            boolean ignoringMethod = this.ignoringMethods.stream()
                    .anyMatch(method->method.equalsIgnoreCase(request.getServletPath())
                            || request.getServletPath().matches(method));
            return allowedMethod || ignoringMethod;
        }
    }

    private CustomSessionRegistry getSessionRegistry(HttpSecurity http) {
        CustomSessionRegistry sessionRegistry = CustomSessionRegistry.getInstance();
        ApplicationContext context = http.getSharedObject(ApplicationContext.class);
        if (context == null) {
            return sessionRegistry;
        }
        if (context.getBeansOfType(DelegatingApplicationListener.class).isEmpty()) {
            return sessionRegistry;
        }
        DelegatingApplicationListener delegating = context
                .getBean(DelegatingApplicationListener.class);
        SmartApplicationListener smartListener = new GenericApplicationListenerAdapter(
                sessionRegistry);
        delegating.addListener(smartListener);

        return sessionRegistry;
    }
}